package com.nulldev.util.web.HttpClient.clients.impld;

import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.nio.channels.ReadableByteChannel;
import java.nio.channels.WritableByteChannel;
import java.util.List;
import java.util.Map;

import com.nulldev.util.JVM.JVM;
import com.nulldev.util.JVM.reflections.Reflections;
import com.nulldev.util.data.Variables;
import com.nulldev.util.io.IOUtils;
import com.nulldev.util.web.HttpClient.HttpResponse;
import com.nulldev.util.web.HttpClient.ResponseFormat;
import com.nulldev.util.web.HttpClient.util.HttpStreamUtil;

public class NativeHttpResponse<T> implements HttpResponse<T> {

	private final Object resp;
	private final ResponseFormat format;

	public NativeHttpResponse(final Object resp, final ResponseFormat format) {
		this.resp = resp;
		this.format = format;
	}

	private static final Reflections httpr_cached;
	static {
		if (JVM.version() >= 11f) {
			httpr_cached = new Reflections("java.net.http.HttpResponse", false);
		} else {
			httpr_cached = null;
		}
	}

	@SuppressWarnings("all")
	private Object invoke(final String o, final Object... v) {
		try {
			return httpr_cached.getClazz().getMethod(o).invoke(this.resp, v);
		} catch (Throwable e) {
			throw new RuntimeException(e);
		}
	}

	@SuppressWarnings("all")
	private Object invoke(final String o, final Class<?>[] clazz, final Object... v) {
		try {
			return httpr_cached.getClazz().getMethod(o, clazz).invoke(this.resp, v);
		} catch (Throwable e) {
			throw new RuntimeException(e);
		}
	}

	@Override
	public int status() {
		return (int) invoke("statusCode");
	}

	@Override
	public boolean successful() {
		return (int) invoke("statusCode") >= 200 && (int) invoke("statusCode") <= 299;
	}

	@Override
	public ResponseFormat format() {
		return this.format;
	}

	@SuppressWarnings("unchecked")
	@Override
	public T data() {
		if (this.format == ResponseFormat.STREAM) {
			String encoding = null;
			try {
				encoding = this.headers().get("content-encoding").get(0);
			} catch (Exception e) {
			}
			if (encoding == null) {
				return (T) invoke("body");
			} else {
				try {
					return (T) HttpStreamUtil.decode((InputStream) (T) invoke("body"), encoding);
				} catch (IOException e) {
					throw new RuntimeException(e);
				}
			}
		} else if (this.format == ResponseFormat.READER) {
			String encoding = null;
			try {
				encoding = this.headers().get("content-encoding").get(0);
			} catch (Exception e) {
			}
			if (encoding == null) {
				return (T) new InputStreamReader((InputStream) invoke("body"));
			} else {
				try {
					return (T) HttpStreamUtil.decode((InputStream) (T) invoke("body"), encoding);
				} catch (IOException e) {
					throw new RuntimeException(e);
				}
			}
		} else if (this.format == ResponseFormat.STRING_VIA_STREAM) {
			String encoding = null;
			try {
				encoding = this.headers().get("content-encoding").get(0);
			} catch (Exception e) {
			}
			if (encoding == null) {
				try {
					return (T) new String(IOUtils.toByteArray((InputStream) (T) invoke("body")));
				} catch (Exception e) {
					throw new RuntimeException(e);
				}
			} else {
				try {
					final InputStream output = HttpStreamUtil.decode((InputStream) (T) invoke("body"), encoding);
					return (T) new String(IOUtils.toByteArray(output));
				} catch (Exception e) {
					throw new RuntimeException(e);
				}
			}
		} else {
			return (T) invoke("body");
		}
	}

	@Override
	public void close() throws Exception {
		if (invoke("body") instanceof InputStream) {
			((InputStream) invoke("body")).close();
		}
		// TODO: Is this needed?
	}

	@Override
	public T dataThenClose() {
		final T data = this.data();
		try {
			this.adaptiveClose();
		} catch (Exception e) {
			throw new RuntimeException(e);
		}
		return data;
	}

	@SuppressWarnings("unchecked")
	@Override
	public Map<String, List<String>> headers() {
		return (Map<String, List<String>>) Reflections.encapsulate(invoke("headers")).invoke("map");
	}

	@SuppressWarnings("unchecked")
	@Override
	public String[] cookies() {
		final List<String> n = ((Map<String, List<String>>) Reflections.encapsulate(invoke("headers")).invoke("map")).get("set-cookie");
		return n.toArray(new String[n.size()]);
	}

	@SuppressWarnings("unchecked")
	@Override
	public T data(final boolean avoidDecoding) {
		if (avoidDecoding) {
			return (T) invoke("body");
		} else {
			return this.data();
		}
	}

	@Override
	public T dataThenClose(final boolean avoidDecoding) {
		final T data = this.data(avoidDecoding);
		try {
			this.close();
		} catch (Exception e) {
			throw new RuntimeException(e);
		}
		return data;
	}

	@Override
	public void adaptiveClose() throws Exception {
		this.close();
	}

	@Override
	public void writeTo(final OutputStream os) throws IOException {
		Variables.requireNonNull(os, "os");
		String encoding = null;
		InputStream is = null;
		try {
			encoding = this.headers().get("content-encoding").get(0);
		} catch (Exception e) {
		}
		if (this.format == ResponseFormat.STREAM || this.format == ResponseFormat.READER)
			is = HttpStreamUtil.decode((InputStream) invoke("body"), encoding);
		else if (this.format == ResponseFormat.BYTES)
			is = new ByteArrayInputStream((byte[]) invoke("body"));
		else if (this.format == ResponseFormat.STRING)
			is = new ByteArrayInputStream(((String) invoke("body")).getBytes());
		else if (this.format == ResponseFormat.STRING_VIA_STREAM)
			is = HttpStreamUtil.decode((InputStream) invoke("body"), encoding);
		else
			throw new UnsupportedOperationException("Cannot writeTo from: " + this.format);
		if (is == null)
			throw new NullPointerException("is == null!");
		IOUtils.autoCopy(is, os);
		IOUtils.closeQuietly(is);
	}

	@Override
	public void writeTo(final OutputStream os, final int preferredBufferSize) throws IOException {
		Variables.requireNonNull(os, "os");
		HttpStreamUtil.__check_valid_buffer_size(preferredBufferSize);
		String encoding = null;
		InputStream is = null;
		try {
			encoding = this.headers().get("content-encoding").get(0);
		} catch (Exception e) {
		}
		if (this.format == ResponseFormat.STREAM || this.format == ResponseFormat.READER)
			is = HttpStreamUtil.decode((InputStream) invoke("body"), encoding);
		else if (this.format == ResponseFormat.BYTES)
			is = new ByteArrayInputStream((byte[]) invoke("body"));
		else if (this.format == ResponseFormat.STRING)
			is = new ByteArrayInputStream(((String) invoke("body")).getBytes());
		else if (this.format == ResponseFormat.STRING_VIA_STREAM)
			is = HttpStreamUtil.decode((InputStream) invoke("body"), encoding);
		else
			throw new UnsupportedOperationException("Cannot writeTo from: " + this.format);
		if (is == null)
			throw new NullPointerException("is == null!");
		IOUtils.autoCopyWithBufferSize(is, os, preferredBufferSize);
		IOUtils.closeQuietly(is);
	}

	@Override
	public void writeTo(final WritableByteChannel os) throws IOException {
		Variables.requireNonNull(os, "os");
		String encoding = null;
		InputStream is = null;
		try {
			encoding = this.headers().get("content-encoding").get(0);
		} catch (Exception e) {
		}
		if (this.format == ResponseFormat.STREAM || this.format == ResponseFormat.READER)
			is = HttpStreamUtil.decode((InputStream) invoke("body"), encoding);
		else if (this.format == ResponseFormat.BYTES)
			is = new ByteArrayInputStream((byte[]) invoke("body"));
		else if (this.format == ResponseFormat.STRING)
			is = new ByteArrayInputStream(((String) invoke("body")).getBytes());
		else if (this.format == ResponseFormat.STRING_VIA_STREAM)
			is = HttpStreamUtil.decode((InputStream) invoke("body"), encoding);
		else
			throw new UnsupportedOperationException("Cannot writeTo from: " + this.format);
		if (is == null)
			throw new NullPointerException("is == null!");
		final ReadableByteChannel iso = IOUtils.copyMakeChannel(is);
		IOUtils.fastCopy(iso, os);
		IOUtils.closeQuietly(iso);
	}

}
